package table

import (
	"encoding/json"
	nest_cache "lol.com/feb214-game/nest.git/cache"
	"lol.com/feb214-game/nest.git/cricket"
	"lol.com/feb214-game/nest.git/leaf"
	"lol.com/feb214-game/nest.git/log"
	"lol.com/feb214-game/nest.git/mg"
	"lol.com/feb214-game/nest.git/proto"
	"lol.com/feb214-game/nest.git/tools/sample"
	"lol.com/feb214-game/nest.git/tools/tz"
	"lol.com/feb214-game/nest.git/user"
	"math/rand"
	"strconv"
	"sync/atomic"
	"talent.com/cube/common/cache"
	"talent.com/cube/common/cxt"
	"talent.com/cube/common/msg"
	"talent.com/cube/conf"
	"talent.com/cube/conf/g"
	Jmg "talent.com/cube/modules/game/internal/table/jmg"
	"time"
)

//同步用户下注(同步增量数据)，同时进行机器人下注
func syncUserBet(tc *cxt.TableContext) {
	if tc.Stage != msg.TERM_STAGE_BET {
		return
	}
	// 每隔一段时间将下注信息（增量）广播到客户端
	interval := sample.RandInt(g.MinSyncBetCycle, g.MaxSyncBetCycle)
	defer tc.Skeleton.AfterFunc(time.Duration(interval)*time.Millisecond, func() {
		syncUserBet(tc)
	})
	//填充机器人下注
	if tc.TermPlan.Len() != 0 {
		leftMS := float64(tc.NextTS - tz.GetNowTsMs())
		count := tc.TermPlan.Len()
		if leftMS > g.MaxSyncBetCycle {
			count = int(float64(count) / leftMS * float64(interval))
		}
		var uc *cxt.UserContext
		for ; count > 0; count-- {
			elem := tc.TermPlan.Front()
			if elem == nil {
				break
			}
			plan := elem.Value.(*msg.BetReq)
			tc.TermPlan.Remove(elem)
			uc = tc.Players.GetPlayer(uint64(plan.Timestamp))
			if uc != nil {
				userBet(uc, tc, plan)
			}
		}
	}
	if len(tc.UserBetBuffer) == 0 {
		return
	}
	toBroad := &msg.BatchUserBetEV{
		Timestamp:  tz.GetNowTsMs(),
		TermNumber: tc.TermNumber,
		UserBets:   tc.UserBetBuffer,
	}
	broadcast(tc, toBroad)
	// 同步完后重置
	tc.UserBetBuffer = make(map[uint64]*msg.BetInfo)
}

//特殊入口：新建一个预热桌子，快速跑完若干轮生成记录
func createPreHeatTable(roomKind, gameType int32) *cxt.TableContext {
	tc := createNewTable(roomKind, gameType)
	if tc == nil {
		return nil
	}
	startTime := tz.GetNowTsMs()
	turn := sample.RandInt(g.MinTablePreHeatTurns, g.MaxTablePreHeatTurns)
	var sumRate int32
	for _, rate := range Jmg.HitIndexConf {
		sumRate += rate
	}
	for i := 0; i < turn-2; i++ {
		diceMap := make(map[int32]int32)
		for i := 0; i < len(Jmg.HitIndexConf); i++ {
			diceIndex := RandWinIndex(Jmg.HitIndexConf, sumRate)
			if _, ok := diceMap[diceIndex]; ok {
				diceMap[diceIndex] = diceMap[diceIndex] + 1
			} else {
				diceMap[diceIndex] = 1
			}
		}
		for diceIndex, _ := range Jmg.HitIndexConf {
			if len(tc.Jmt.WinHistory[diceIndex].WinHistoryIndex) >= g.WinHistorySize {
				tc.Jmt.WinHistory[diceIndex].WinHistoryIndex = tc.Jmt.WinHistory[diceIndex].WinHistoryIndex[1:]
			}
			if diceMap[diceIndex] >= 2 {
				tc.Jmt.WinHistory[diceIndex].WinHistoryIndex = append(tc.Jmt.WinHistory[diceIndex].WinHistoryIndex, diceMap[diceIndex])
			} else {
				tc.Jmt.WinHistory[diceIndex].WinHistoryIndex = append(tc.Jmt.WinHistory[diceIndex].WinHistoryIndex, 0)
			}
		}
	}
	robotList := cache.GetRobotList(tc.GameType, tc.ID)
	robotConf := conf.RobotConfig(tc.GameType)
	tc.RichUsers = make([]*msg.UserST, g.InitRichUserCount)
	testBetMap := make(map[uint64]int64, g.InitRichUserCount)
	winners := make([]uint64, 0)
	for i := 0; i < g.InitRichUserCount; i++ {
		level := robotConf.GetRandomLevel()
		robotUst := robotList.Dequeue(level)
		testBetMap[robotUst.ID] = rand.Int63n(1500) + 100
		winners = append(winners, robotUst.ID)
		creditRange := robotConf.LevelConfig[level].InitCredit
		credit := robotUst.Credit / g.CoinRate
		robotUst.BetInfo = make(map[int32]*msg.ChipInfo)
		uc := &cxt.UserContext{
			UserST:     robotUst,
			GameType:   tc.GameType,
			RoomKind:   tc.Kind,
			TableID:    tc.ID,
			RobotLevel: level,
		}
		if credit < creditRange[0] || credit > creditRange[1] {
			robotUst.Credit = sample.RandInt64(creditRange[0], creditRange[1]) * g.CoinRate
		}
		//直接加进来
		tc.Players.SetPlayers(robotUst.ID, uc)
	}
	tc.HistoryBetters.PushBack(testBetMap)
	tc.HistoryWinners.PushBack(winners)

	log.Info("createPreHeatTable.init history tableId : %v, end: %v, ", tc.ID, tz.GetNowTsMs()-startTime)

	//for i := 0; i < 6; i++ {
	//	//最后几局加几个机器人生成榜单
	//	if fin := updateRobots(tc); fin != nil {
	//		fin.Wait()
	//	}
	//	// 服务器关闭，则关闭桌子
	//	if atomic.LoadInt32(&g.ServerClosed) == 1 {
	//		closeTable(tc, true)
	//		return nil
	//	}
	//	termNumber, err := cache.IncrTermNo(tc.GameType, tc.Kind)
	//	if err != nil {
	//		log.Error("can't generate termNumber, close table !!!err:%v", err)
	//		closeTable(tc, true)
	//		return nil
	//	}
	//	//重置数据
	//	tc.TermNumber = termNumber
	//	tc.BetInfo = make(map[int32]*msg.ChipInfo)
	//	tc.CostCache = make(map[uint64]int64)
	//	tc.IndexBetCache = make(map[int32]int64)
	//	tc.Players.Map.Range(func(key, value interface{}) bool {
	//		player := value.(*cxt.UserContext)
	//		player.Award = 0
	//		player.Tax = 0
	//		player.BetInfo = make(map[int32]*msg.ChipInfo)
	//		return true
	//	})
	//	//计算下注计划
	//	planRobotBet(tc)
	//	//机器人下注
	//	for elem := tc.TermPlan.Front(); elem != nil; elem = elem.Next() {
	//		plan := elem.Value.(*msg.BetReq)
	//		tc.TermPlan.Remove(elem)
	//		uc := tc.Players.GetPlayer(uint64(plan.Timestamp))
	//		if uc != nil {
	//			userBet(uc, tc, plan)
	//		}
	//	}
	//
	//	//结算
	//	tc.WorldImpl.Settle()
	//	//更新历史记录
	//	if tc.HistoryBetters.Len() >= g.TopTermCount {
	//		tc.HistoryBetters.Remove(tc.HistoryBetters.Front())
	//	}
	//	tc.HistoryBetters.PushBack(tc.CostCache)
	//	if tc.HistoryWinners.Len() >= g.TopTermCount {
	//		tc.HistoryWinners.Remove(tc.HistoryWinners.Front())
	//	}
	//	//赢家统计
	//	winners := make([]uint64, 0)
	//	for playerId, cost := range tc.CostCache {
	//		player := tc.Players.GetPlayer(playerId)
	//		if player.Award > cost {
	//			winners = append(winners, playerId)
	//		}
	//	}
	//	tc.HistoryWinners.PushBack(winners)
	//}
	//log.Info("createPreHeatTable.init history tableId : %v, end: %v, ", tc.ID, tz.GetNowTs()-startTime)
	//log.Info("[%v]room %v table %v pre-heat done", g.GetCodeLabel(tc.GameType), tc.Kind, tc.ID)
	return tc
}

//通用新一轮游戏入口
func startNewTerm(tc *cxt.TableContext) {
	if atomic.LoadInt32(&g.ServerClosed) == 1 {
		closeTable(tc, true)
		return
	}
	userCount := int(atomic.LoadInt32(&tc.UserCount))
	//没有真实玩家则关闭桌子
	if tc.NoUserCount >= g.MaxNoUserCloseTableCount && userCount == 0 {
		log.Info("超时")
		closeTable(tc, false)
		return
	}
	if userCount == 0 {
		tc.NoUserCount += 1
	} else {
		tc.NoUserCount = 0
	}

	//开始游戏前，就计算出用户的冲提比，避免下注后计算导致计算偏差
	userStats := g.StatsDAO.GetUserStats(tc.RealUserId)
	user, _ := g.GetAccountDAO(g.Jmg).GetUser(tc.RealUserId)
	recharge, _, rechargeRate, _ := rechargePer(userStats, user)
	tc.RechargeRate = rechargeRate
	tc.RechargeAmount = recharge

	tc.Stage = msg.TERM_STAGE_BET
	nextTS := tc.WorldImpl.NextTS()
	now := tz.GetNowTsMs()
	//⏱定时结算
	tc.Skeleton.AfterFunc(time.Duration(nextTS-now)*time.Millisecond, func() {
		settle(tc)
	})
	//更新数据
	tc.TermNumber += 1
	tc.NextTS = nextTS
	tc.BetInfo = make(map[int32]*msg.ChipInfo)
	tc.CostCache = make(map[uint64]int64)
	tc.IndexBetCache = make(map[int32]int64)
	tc.RealIndexBetCache = make(map[int32]int64)
	//重置用户
	tc.Players.Map.Range(func(key, value interface{}) bool {
		playerId := key.(uint64)
		player := value.(*cxt.UserContext)
		player.Award = 0
		player.Tax = 0
		player.BetInfo = make(map[int32]*msg.ChipInfo)
		player.SyncBet = 0
		//NOTE: 内存数据仅为断线用户保留到当局结束
		// 这可能会导致机器人在同步到数据库之前余额被重新初始化，目前忽略这种错误
		// 如果需要修复，对机器人也构造计数器即可
		if atomic.LoadInt32(&player.Disconnect) == 1 &&
			(player.CostSyncCounter == nil || player.CostSyncCounter.Cleared()) {
			removeTableUser(tc, playerId)
		}
		return true
	})
	//刷新机器人
	if fin := updateRobots(tc); fin != nil {
		go fin.Wait()
	}
	//通知开始
	broadcast(tc, &msg.StartBetNT{
		Timestamp:  now,
		TermNumber: tc.TermNumber,
		NextTS:     nextTS,
		Turn:       tc.Turn,
	})
	//给动画延迟时间
	//time.Sleep(2 * time.Second)
	tc.Skeleton.AfterFunc(g.GameBeginDelay, func() {
		userCount := int(atomic.LoadInt32(&tc.UserCount))
		robotCount := tc.Players.Length() - userCount
		log.Info("[%v]room %v table %v term %v start, now user count:%v, robot count:%v",
			g.GetCodeLabel(tc.GameType), tc.Kind, tc.ID, tc.TermNumber, userCount, robotCount)
		nest_cache.StreamTrack(g.KafkaPool, g.TopicCommon, strconv.Itoa(int(tc.TermNumber)), cricket.NewTermTrack{
			Type:       "new_round",
			Ts:         time.Now().Unix(),
			TableId:    tc.ID,
			RoomIndex:  tc.Kind,
			TermNumber: tc.TermNumber,
			UserCount:  userCount,
			RobotCount: robotCount,
			GameType:   tc.GameType,
		})
		//更新坐席
		updateTopPlayer(tc)
		//记录当期
		cache.RecordTerm(tc.GameType, tc.TermNumber, "")
		//计算下注计划
		planRobotBet(tc)
		//开始同步下注信息
		syncUserBet(tc)
	})
}

//对不同玩家生成不同的广播结算信息
func genAwardMap(tc *cxt.TableContext, userId uint64, specialUser map[uint64]*msg.UserST) map[uint64]*msg.UserAward {
	awardMap := make(map[uint64]*msg.UserAward, len(specialUser)+1) //加上自己、庄家和神算子
	for uid, ust := range specialUser {
		awardMap[uid] = &msg.UserAward{
			Cost:  tc.CostCache[uid],
			Award: ust.Award,
			After: ust.Credit,
		}
	}
	if specialUser[userId] == nil {
		awardMap[userId] = &msg.UserAward{
			Cost:  tc.CostCache[userId],
			Award: tc.Players.GetPlayer(userId).Award,
			After: tc.Players.GetPlayer(userId).Credit,
		}
	}
	return awardMap
}

//通用结算入口
func settle(tc *cxt.TableContext) {
	tc.Stage = msg.TERM_STAGE_AWARD
	//最后一次同步
	if len(tc.UserBetBuffer) != 0 {
		broadcast(tc, &msg.BatchUserBetEV{
			Timestamp:  tz.GetNowTsMs(),
			TermNumber: tc.TermNumber,
			UserBets:   tc.UserBetBuffer,
		})
		tc.UserBetBuffer = make(map[uint64]*msg.BetInfo)
	}
	//通知结算
	broadcast(tc, &msg.StopBetNT{
		Timestamp:  tz.GetNowTsMs(),
		TermNumber: tc.TermNumber,
	})
	//在各自游戏中结算
	//结算完毕后再更新nextTS
	awardNT := tc.WorldImpl.Settle()
	nextTS := tc.WorldImpl.NextTS()
	//⏱开启新的一轮
	tc.Skeleton.AfterFunc(time.Duration(nextTS-tz.GetNowTsMs())*time.Millisecond, func() {
		startNewTerm(tc)
	})
	tc.NextTS = nextTS
	awardNT.NextTS = nextTS
	//广播结算结果，7=6个坐席+1个庄家
	specialUser := make(map[uint64]*msg.UserST, 7)
	for _, ust := range tc.RichUsers {
		specialUser[ust.ID] = ust
	}
	if tc.BestUser != nil {
		specialUser[tc.BestUser.ID] = tc.BestUser
	}
	banker := tc.BankerList[0]
	specialUser[banker.ID] = banker
	// 神算子更新
	topInfo := updateTopPlayerSettle(tc)
	tc.RoomST.TopInfo = topInfo
	// 推送用户奖励信息到客户端
	tc.Players.Map.Range(func(key, value interface{}) bool {
		playerId := key.(uint64)
		player := value.(*cxt.UserContext)
		if player.IsRobot() {
			return true
		}
		awardMap := genAwardMap(tc, playerId, specialUser)
		//由于广播是异步的，这里要用值类型
		nt := *awardNT
		nt.AwardInfo = awardMap
		nt.BetInfo = player.BetInfo
		nt.TopInfo = topInfo

		pushUser(playerId, &nt)
		return true
	})
	//统计数据
	var (
		realWinCount int
		player       *cxt.UserContext

		winners   = make([]uint64, 0)
		allAwards = make(map[uint64]int64, tc.Players.Length())
		//needDecr  = g.IsAwardLoseGame(tc.GameType)
	)
	for playerId, cost := range tc.CostCache {
		player = tc.Players.GetPlayer(playerId)
		//没有输钱的用户将额外增加的一次移除
		if !player.IsRobot() && player.Award >= 0 {
			player.CostSyncCounter.Decr()
		}
		//赢家统计
		if player.Award > cost {
			winners = append(winners, playerId)
		}
		allAwards[playerId] = player.Award
		if !player.IsRobot() {
			realWinCount++
		}
	}
	//更新历史记录
	if tc.HistoryBetters.Len() >= g.TopTermCount {
		tc.HistoryBetters.Remove(tc.HistoryBetters.Front())
	}
	tc.HistoryBetters.PushBack(tc.CostCache)
	if tc.HistoryWinners.Len() >= g.TopTermCount {
		tc.HistoryWinners.Remove(tc.HistoryWinners.Front())
	}
	tc.HistoryWinners.PushBack(winners)
	//记录开奖结果
	if realWinCount > 0 {
		result, _ := json.Marshal(allAwards)
		cache.RecordTerm(tc.GameType, tc.TermNumber, string(result))
	} else {
		cache.RemoveTerm(tc.GameType, tc.TermNumber)
	}
	//异步落库
	for playerId, award := range allAwards {
		p := tc.Players.GetPlayer(playerId)
		level := p.RobotLevel
		if !p.IsRobot() {
			toSyncBet := atomic.SwapInt64(&p.UnSync, 0)
			termBet := tc.CostCache[playerId]
			//结算前，优先结算下注，因为chan是有序的，可以保证下注在award前面
			data := &cxt.SyncData{
				UserId:         playerId,
				GameType:       tc.GameType,
				RobotLevel:     level,
				Bet:            -toSyncBet,
				TermBet:        -termBet,
				Award:          award,
				TermNumber:     tc.TermNumber,
				TableID:        tc.ID,
				WithdrawalRate: p.WithdrawalRate,
			}
			g.ServerCloseWG.Add(1)
			if level < 0 {
				realWinCount--
				data.Left = realWinCount
			} else {
				data.Left = 1 //机器人就算没同步完毕影响也不大
			}
			cxt.GetTermChannel(playerId) <- data
		}
	}
	sleepRobot(tc)
}

//关闭桌子
func closeTable(tc *cxt.TableContext, force bool) {
	if force {
		atomic.StoreInt32(&tc.UserCount, g.TableMaxUserCount)
	}
	robotList := cache.GetRobotList(tc.GameType, tc.ID)
	cache.DeleteRobotListByTableId(tc.ID)
	tc.Players.Map.Range(func(key, value interface{}) bool {
		playerId, player := key.(uint64), value.(*cxt.UserContext)
		if player.IsRobot() {
			tc.Players.DeletePlayers(playerId)
			robotList.Enqueue(player.UserST, player.RobotLevel, 0)
		} else {
			//如果有真实用户，应该是在关闭服务器
			player.TableID = 0
			ag := cxt.GetUserAgent(playerId)
			if ag != nil {
				leaf.CloseAgent(ag, proto.STATUS_SERVER_CLOSED, "", playerId)
			}
		}
		return true
	})
	close(tc.CloseChan)
	g.ServerCloseWG.Done()
	g.AllTables.Delete(tc.ID)
	log.Info("closed room %v game %v table %v, force: %v", tc.Kind, g.GetCodeLabel(tc.GameType), tc.ID, force)
}

func IsTimeOut(tc *cxt.TableContext) bool {
	return tz.GetNowTs()-tc.LastActiveAt > g.RoomIdleLimit
}

func rechargePer(userStats *mg.UserStats, user *user.Account) (int64, int64, float64, bool) {
	recharge := userStats.Recharge.Total
	withdraw := userStats.Withdraw.Total
	//recharge, withdraw := mg.GetAmountD(userStats)
	//提现中
	processing, _ := g.GetAccountDAO(g.Jmg).GetProcessingWithdraw(userStats.ID)
	//提现金额 = 已经提现 + 提现中 + 可提现
	finalWithdraw := withdraw + processing + user.WithdrawableAmount
	log.Info("userId:%v ,finalRecharge:%v,finalWithdraw:%v", user.ID, recharge, finalWithdraw)
	rechargeRate := float64(finalWithdraw) / float64(recharge)
	var isNoWithdraw bool
	//如果用户充值 >0 并且没有tx，就是 用户未完成首次提现
	if recharge > 0 && withdraw+processing == 0 {
		isNoWithdraw = true
	}
	return recharge, finalWithdraw, rechargeRate, isNoWithdraw
}

func RandWinIndex(betRate map[int32]int32, sumRate int32) int32 {
	ra := rand.Int31n(sumRate)
	for i, rate := range betRate {
		if ra < rate {
			return i
		}
		ra -= rate
	}
	return -1
}
